/**
 * File: StatisticsProcessor.java
 * Created by: mhaimel
 * Created on: 31 Mar 2009
 * CVS:  $Id: StatisticsProcessor.java,v 1.6 2009/11/24 15:52:43 mhaimel Exp $
 */
package uk.ac.ebi.velvet.processor;

import static uk.ac.ebi.curtain.utils.StatisticHelper.sort;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;

import javolution.util.FastMap;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import uk.ac.ebi.curtain.utils.file.EntryMapperBean;
import uk.ac.ebi.velvet.VelvetUncheckedException;
import uk.ac.ebi.velvet.processor.ExpectedCoverageStrategy.ExpectedCoverageInformation;
import uk.ac.ebi.velvet.processor.MinCutoffStrategy.MinimumCutoffInformation;
import uk.ac.ebi.velvet.processor.impl.StatisticFileParser;
import uk.ac.ebi.velvet.strategy.ExpectedWeightedCovergeStrategy;

/**
 * <p>
 * Reads the statistics file and calculates the expected coverage. <br>
 * The assumption is, that at this point the low coverage regions are 
 * not included any more because the program only uses reads, which mapped 
 * to the contigs.</p>
 * <p>
 * The coverage with the best score (count of coverage weighted by length) 
 * in a predefined range will be chosen as expected coverage. 
 * </p>
 *  This is a stateful object.
 * @author mhaimel
 *
 */
public class StatisticsProcessor {
	public static final MinCutoffStrategy STRATEGY_MIN_WEIGHTED = new MinCutoffStrategy(){@Override
	public Integer calculateMinCutoff(MinimumCutoffInformation info) {
//		int i = (int) Math.round((double)info.getExpectedCoverage()/2); // _1
//		int i = (int) Math.round((double)info.getExpectedCoverage()/2); // _1
//		int i = info.getExpectedCoverage()/2; // _2
//		int i = info.getExpectedCoverage()/3; // _3
		int i = (int) Math.round((double)info.getExpectedCoverage()/3); // _4
		return i>2?i:2;
	}}; 
//		new MinWeightedCoverageStrategy();
	public static final ExpectedCoverageStrategy STATEGY_EXP_WEIGHTED = new ExpectedWeightedCovergeStrategy(4);
	private static final String _INF_VAL = "Inf";
	private static final int _LGTH_POS = 1;
	private static final int _SHORT1_COV_POS = 5;
	private Log log = LogFactory.getLog(this.getClass());
	private File statsFile;
	private Integer minCov = 0;
	private Integer maxCov = 500;
	private MinCutoffStrategy minCutoffStrategy = STRATEGY_MIN_WEIGHTED;
	private ExpectedCoverageStrategy expCovStrategy = STATEGY_EXP_WEIGHTED;

	
	private Map<Integer, List<Integer>> coverage2ScoreMap = new FastMap<Integer, List<Integer>>();

	public StatisticsProcessor(File statsFile) {
		this(statsFile,null);
	}	

	
	public StatisticsProcessor(File statsFile, StatisticHint hint) {
		this.statsFile = statsFile;
//		if(null != hint){
//			expCovStrategy = new ExpCovInRangeCalculator(hint, expCovStrategy);
//			minCutoffStrategy = new MinCutoffInRangeCalculator(hint,minCutoffStrategy);
//		}
	}
	
	/**
	 * Processes the statistics file and returns statistics information
	 * uses {@link #gatherStats(File)} and {@link #processStatistics()} to retrieve information
	 * @return StatisticsInfo
	 */
	public StatisticsInfo process(){
		getLog().debug("Process statistics for " + getStatsFile() + " ..."); 
		gatherStats(getStatsFile());
		sort(getCoverage2ScoreMap());		
		return processStatistics();
	}
	
	public void reset(){
		getCoverage2ScoreMap().clear();
	}
	

	public StatisticsInfo processStatistics(Long maxTotal){
		if(hasData()){
			List<Integer> arr = new ArrayList<Integer>();
			for(Entry<Integer,List<Integer>> entry : getCoverage2ScoreMap().entrySet()){
				arr.addAll(entry.getValue());
			}
			Collections.sort(arr);
			Collections.reverse(arr);
			int i = 0;
			int size = arr.size();
			long total = 0;
			for(i = 0; i < size; ++i){
				if(total >= maxTotal){
					break;
				}
				total += arr.get(i);
			} // TODO if total not reached
			LinkedList<Integer> arr2 = new LinkedList<Integer>(arr.subList(0, i));
			for(Entry<Integer,List<Integer>> entry : getCoverage2ScoreMap().entrySet()){
				List<Integer> tmpA = entry.getValue();
				Collections.sort(tmpA);
				Collections.reverse(tmpA);
				i = 0;
				int j = 0;
				for(i = 0; i < tmpA.size() && j < arr2.size(); ++i){
					Integer a = tmpA.get(i);
					Integer b = arr2.get(j);						
					while(a < b && ++j < arr2.size()){
						b = arr2.get(j); // TODO change a b 
					}
					if (a.equals(b)){
						arr2.remove(j);
					}
				}
				List<Integer> v = new ArrayList<Integer>(tmpA.subList(0, i));
				tmpA.clear();
				tmpA.addAll(v);
			}
		}
		return processStatistics();
	}
	
	public StatisticsInfo processStatistics(){
		StatisticsInfo info = new StatisticsInfo();
		if(hasData()){		
			// do process
			fillBaseStats(info);
	
			info.setExpCov(calculateExpCov(info));	
			info.setCutoffCov(calculateMinCutoff(info));
			
		}
		return info;
	}

	/**
	 * Fill {@link StatisticsInfo#setMax(Integer)} {@link StatisticsInfo#setTotalLength(Long)} and 
	 * {@link StatisticsInfo#setN50(Integer)}
	 * @param info
	 */
	private void fillBaseStats(StatisticsInfo info) {
		int max = -1;
		int min = -1;
		int covMax = -1;
		long totalLen = 0;
		List<Integer> list = new ArrayList<Integer>();
		for(Entry<Integer,List<Integer>> entry : getCoverage2ScoreMap().entrySet()){
			for(Integer len : entry.getValue()){
				list.add(len);
				if(len < min || min < 0){
					min = len;
				}
				if(max < len){
					max = len;
					covMax = entry.getKey();
				}
				totalLen += len;
			}
		}
		info.setMinLength(min);
		info.setNContigs(list.size());
		Integer n50 = calculateN50(list,totalLen);
		info.setN50(n50);
		info.setTotalLength(totalLen);
		if(covMax > 0 && max > 0){
			info.setCoverageOfMaxLength(covMax);
			info.setMaxLength(max);
		}
	}


	private boolean hasData() {
		for(List<Integer> lenList : getCoverage2ScoreMap().values()){
			if(!lenList.isEmpty()){
				return true;
			}
		}
		return false;
	}


	public Integer calculateExpCov(StatisticsInfo info) {
		if(hasData()){
			ExpectedCoverageInformation expInfo = new ExpectedCoverageInformation();
			expInfo.setCov2Score(getCoverage2ScoreMap());
//			expInfo.setCoverageCutoff(info.getCutoffCov());
			return getExpCovStrategy().calculateExpCov(expInfo);
		} else {
			getLog().debug("No coverage available to calculate Expected coverage!");
			return null;
		}
	}


	public Integer calculateMinCutoff(StatisticsInfo info) {
		if(hasData()){
			MinimumCutoffInformation minInfo = new MinimumCutoffInformation();
//			minInfo.setCov2Score(getCoverage2ScoreMap());
//			minInfo.setMaxLength(info.getMaxLength());
//			minInfo.setCoverageMaxLength(info.getCoverageOfMaxLength());
//			minInfo.setTotalLength(info.getTotalLength());
			minInfo.setExpectedCoverage(info.getExpCov());
			return getMinCutoffStrategy().calculateMinCutoff(minInfo);
		} else {
			getLog().debug("No coverage available to calculate minimum coverage cutoff!");
			return null;
		}
	}
	
	public void gatherStats(BufferedReader in) throws IOException{
		StatisticFileParser fp = new StatisticFileParser();
		fp.parse(in);
		gatherStats(fp);
	}
	
	public void gatherStats(File file) {
		StatisticFileParser fp = new StatisticFileParser();
		fp.parse(file);
		gatherStats(fp);
	}
	
	private void gatherStats(StatisticFileParser fp){
		Integer[][] arrList = new Integer[fp.getCoverageKeys().size()+1][];
//		String[] arr = new String[2];
		arrList[0] = fp.getLengthList().toArray(new Integer[fp.getLengthList().size()]);
		
		Collection<String> covKeys = fp.getCoverageKeys();
		
		//check if all have the same length
		int i = 0;
		for(String key : covKeys){
			++i;
			List<Integer> list = fp.getCoverageListFor(key);
			if(!Integer.valueOf(list.size()).equals(arrList[0].length)){
				throw new VelvetUncheckedException(
						"Different list length for " 
						+ key 
						+ ": "
						+list.size() 
						+ " instead of " 
						+ arrList[0].length);
			}
			arrList[i] = list.toArray(new Integer[list.size()]);
		}
		
		for(i = 0; i < arrList[0].length; ++i){
			int len = arrList[0][i];
			int covSum = 0;
			for(int j = 1; j < arrList.length; ++j){
				covSum += arrList[j][i];
			}
			addSequence(covSum, len);
		}
	}
	
	public void addEntry(EntryMapperBean sequ){
		addSequence(1, sequ.getSequenceLength());
	}
	
	protected void addSequence(Integer cov1, Integer len) {
		if(validCoverage(cov1) && validLength(len)){
			List<Integer> val = getCoverage2ScoreMap().get(cov1);
			if(null == val){
				val = new ArrayList<Integer>();
				getCoverage2ScoreMap().put(cov1, val);
			}
			val.add(len);
		}
	}


	private boolean validLength(Integer len) {
		return null != len && len > 0;
	}


	private Log getLog() {
		return log;
	}

	protected boolean validCoverage(Integer cov1) {
		return null != cov1 && cov1 < getMaxCov() && cov1 > getMinCov();
	}
	
	private boolean isHeader(String line) {
		return line.startsWith("ID");
	}

	/**
	 * Calculates N50 for given increasing sorted list
	 * @param list
	 * @param total
	 * @return n50 Integer
	 */
	public Integer calculateN50(List<Integer> list, long total) {
		Collections.sort(list);
		Long mid = total/2;
		int iCnt = list.size()-1;
		Integer curr = 0;
		
		// this implementation steps one  
		while((curr += list.get(iCnt)) < mid){
			--iCnt;
		}
		return list.get(iCnt);
	}
	
	public Map<Integer, List<Integer>> getCoverage2ScoreMap() {
		return coverage2ScoreMap;
	}
	public MinCutoffStrategy getMinCutoffStrategy() {
		return minCutoffStrategy;
	}
	public void setMinCutoffStrategy(MinCutoffStrategy minCutoffStrategy) {
		this.minCutoffStrategy = minCutoffStrategy;
	}
	public ExpectedCoverageStrategy getExpCovStrategy() {
		return expCovStrategy;
	}
	public void setExpCovStrategy(ExpectedCoverageStrategy expCovStrategy) {
		this.expCovStrategy = expCovStrategy;
	}

	private static Pattern tabPat = Pattern.compile("\t");
	private String[] split(String line) {
		return tabPat.split(line);
	}

	private File getStatsFile() {
		return this.statsFile;
	}	
	
	private Integer extLength(String[] row){
		return Integer.valueOf(row[_LGTH_POS]);
	}
	
	private Integer extCov1(String[] row){
		String entry = row[_SHORT1_COV_POS];
		if(StringUtils.equals(entry, _INF_VAL)){
			return null;
		}
		Double val = Double.valueOf(entry);
		if(val.isInfinite() || val.isNaN()){
			return null;
		} else {
//			return val.intValue();

			// Don't round, otherwise it you could loose data for 
			// supporting the right boundary instead of the left one
			// TODO - check
			return Long.valueOf(Math.round(val)).intValue();
		}
	}

	public Integer getMinCov() {
		return minCov;
	}

	public void setMinCov(Integer minCov) {
		this.minCov = minCov;
	}

	public Integer getMaxCov() {
		return maxCov;
	}

	public void setMaxCov(Integer maxCov) {
		this.maxCov = maxCov;
	}
	
	public static class StatisticHint {
		private Integer expCovMin = null;
		private Integer expCovMax = null;
		private Integer covCutoffMax = null;
		private Integer covCutoffMin = null;
		public StatisticHint(Integer expCovMin, Integer expCovMax,
				Integer covCutoffMax, Integer covCutoffMin) {
			super();
			this.expCovMin = expCovMin;
			this.expCovMax = expCovMax;
			this.covCutoffMax = covCutoffMax;
			this.covCutoffMin = covCutoffMin;
		}
		public Integer getExpCovMin() {
			return expCovMin;
		}
		public Integer getExpCovMax() {
			return expCovMax;
		}
		public Integer getCovCutoffMax() {
			return covCutoffMax;
		}	
		public Integer getCovCutoffMin() {
			return covCutoffMin;
		}
	}
}
